Skip to content

💥 脚本猫 AI Agent#1324

Merged
CodFrm merged 176 commits intorelease/v1.4-agentfrom
feature/agent
Apr 7, 2026
Merged

💥 脚本猫 AI Agent#1324
CodFrm merged 176 commits intorelease/v1.4-agentfrom
feature/agent

Conversation

@CodFrm
Copy link
Copy Markdown
Member

@CodFrm CodFrm commented Mar 31, 2026

概述

CAT.agent 系列API、设计并不稳定,可能会进行调整

为 ScriptCat 新增完整的 AI Agent 平台,用户可以通过自然语言对话控制浏览器,自动完成网页操作、定时任务、工具调用等复杂工作流。

构建产物可以从 https://github.com/scriptscat/scriptcat/actions/workflows/build.yaml feature/agent 获取

Youtube 演示

https://www.youtube.com/watch?v=6OT7qeY3Uuk

如果你对此感兴趣,可以加入 Discord 进行交流

核心功能

🤖 AI 对话与多模型支持

  • 内置聊天界面,支持流式输出、Markdown 渲染、思考过程展示
  • 适配 OpenAI 和 Anthropic (Claude) 两大主流 LLM Provider
  • 支持 SSE 流式解析,实时展示 Agent 推理过程

🌐 浏览器 DOM 操作

  • Agent 可通过自然语言指令操作网页:点击、填写表单、滚动、等待元素、截图等
  • 双引擎实现:基于内容脚本的 DOM 操作 + 基于 CDP(Chrome DevTools Protocol)的底层控制
  • 网页截图能力,Agent 可"看到"页面内容辅助决策

🧩 Skill 技能系统

  • 模块化能力扩展机制:一个 Skill = 行为提示词 + 可选工具脚本 + 可选参考文档
  • 支持一键安装 / 管理 / 卸载
  • Agent 根据对话内容自动匹配并加载对应 Skill

🔧 CATTool 自定义工具

  • 用户可编写自定义工具脚本(CATTool),Agent 在对话中自动发现和调用
  • 标准化元数据格式,支持参数类型声明和 GM API 权限申请
  • 工具注册中心统一管理,命名空间隔离避免冲突

🔌 MCP 协议支持

  • 实现 MCP(Model Context Protocol)客户端
  • 可连接外部 MCP 服务器,扩展 Agent 的工具调用能力
  • MCP 服务器管理界面,支持增删和状态监控

⏰ 定时任务调度

  • 通过自然语言创建 Cron 定时任务
  • 支持任务的创建、查询、更新、删除
  • 任务到期自动触发 Agent 执行

📂 OPFS 文件系统

  • 基于浏览器 OPFS(Origin Private File System)的文件管理
  • Agent 可读写文件,支持跨会话数据持久化
  • 内置文件浏览器 UI

💬 GM API 扩展(脚本侧 Agent API)

  • 新增 CAT.agent.* 系列 API,用户脚本可与 Agent 深度交互:
    • CAT.agent.dom — 脚本内调用 DOM 操作
    • CAT.agent.conversation — 管理对话上下文
    • CAT.agent.tools — 动态注册/调用工具
    • CAT.agent.task — 任务生命周期管理

UI 页面

页面 功能
AgentChat 聊天主界面(消息列表、输入框、工具调用展示、思考过程折叠)
AgentProvider 模型配置(API Key、模型选择、参数调节)
AgentSkills Skill 安装与管理
AgentCATool CATTool 管理
AgentMcp MCP 服务器管理
AgentTasks 定时任务管理
AgentOPFS 文件浏览器

技术亮点

  • 架构清晰:Agent 编排器 → Provider 适配 → Tool 注册 → 执行器,各层职责明确
  • 测试完善:单元测试覆盖核心模块(工具注册、CATTool 执行、MCP 客户端、SSE 解析等),新增 E2E 测试覆盖对话流程、工具调用、Skill 安装、错误处理
  • 类型安全:完整的 TypeScript 类型定义(scriptcat.d.ts +1193 行),同步维护中文版类型注释
  • 国际化:8 种语言的翻译同步更新

数据规模

  • 212 个文件变更,+30,880 行,-2,158 行
  • 80+ 个 commit

CodFrm and others added 30 commits March 13, 2026 23:16
- gm-api.spec.ts: Phase 2 重启 context 后等待 service worker 注册完成
  再交给 fixtures,避免 extensionId fixture 用 10s 全局超时等待失败
- gm-api.spec.ts: 用事件驱动 Promise 替换 500ms 轮询循环,
  console 结果一出现立即继续
- utils.ts: installScriptByCode 用 DOM 事件等待替代固定延迟:
  click 后等光标出现,粘贴后等 .view-lines 内容变化

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
新增 AI Agent 聊天模块,包括聊天界面、服务层、数据存储和多语言支持
将 Agent API 请求路由到 runtime/gmApi 通道,通过 @PermissionVerify.API
装饰器实现权限验证,脚本首次调用时弹窗确认,防止未授权消耗 API Token。
- 新增 OPFS 文件浏览器页面,支持目录浏览、文件预览和删除操作
- 默认进入 agents/ 目录,支持面包屑导航到根目录
- 支持在 conversation.create 时注册工具,简化多轮对话的工具传递
- 添加 8 种语言的 i18n 翻译
- 修复 callLLM 提前转发 done 事件导致客户端过早 resolve 的问题
- 修复 WindowMessage.connect 未使用 "*" targetOrigin 导致沙箱消息被丢弃
- 修复 tool calling 循环中 assistant 消息缺少 tool_calls 字段
- 支持 OpenAI 和 Anthropic 格式的 tool_calls 消息构建
- UI 合并 tool 结果到 assistant 的 ToolCallBlock 展示,过滤 tool/system 消息
- 提取 buildInstance 函数解决装饰器 this 绑定问题,使用 uuidv4 生成 ID
- ChatMessage 类型新增 usage/durationMs/firstTokenMs 字段
- Service Worker 测量 LLM 调用总耗时并通过 done 事件返回
- UI 层捕获首 token 延迟和 token 用量,持久化到消息中
- 新增 MessageToolbar 组件:左侧操作按钮(复制/重新回答/删除),右侧元数据信息(token用量/耗时/TTFT/工具调用数)
- 流式期间显示实时计时动画,完成后切换为最终数据
- 删除操作支持二次确认
- 新增 deleteMessages 批量删除辅助函数
- 添加 i18n 多语言支持(8个locale)
- 新增 CATTool 元数据解析、存储、注册和执行完整链路
- 新增 CAT.agent.tools GM API(install/remove/list/call)
- 修复 CATTool 沙箱执行时 args 未定义的问题:使用 compileScriptCodeByResource 包裹代码以启用 with 上下文绑定
- 优化 Agent Chat UI 组件样式
将 App.tsx (~1130行) 按职责拆分为:
- utils.ts: 纯工具函数和常量
- hooks.tsx: useInstallData() 自定义 hook
- components/CATToolInstallView.tsx: CATTool 安装视图
- components/ScriptInstallView.tsx: UserScript/Subscribe 安装视图
- App.tsx: 精简为路由分发 (~57行)
新增 example/agents/tools/ 目录:
- hello_world.js: 最简 CATTool 示例
- text_processor.js: 多参数和 enum 类型
- json_formatter.js: JSON 处理和错误处理
- weather_query.js: GM_xmlhttpRequest 网络请求
- use_cattool.js: 通过脚本 API 安装和调用 CATTool
- README.md: 格式说明和测试方法
- 修复历史消息未携带 toolCalls 字段导致多轮 tool calling 上下文丢失
- 添加 OpenAI 流式 API 错误响应处理
- 添加 /new 命令和 clearMessages 接口用于清空对话上下文
- UI 发送消息时正确传递 toolCallId 和 toolCalls
- 删除独立的 handleChat,UI 和 Sandbox 统一走 conversationChat 通道
- 持久化责任从 UI 移交到 SW,简化前端逻辑
- 字段命名统一:createdAt/updatedAt → createtime/updatetime
- 支持 UI 动态切换 modelId
- 大幅补充 Agent 相关测试(流式解析、tool calling 循环、CATTool 边界等)
- 对话实例支持 / 命令拦截机制,内置 /new 命令清空对话
- 脚本可通过 commands 选项注册自定义命令并覆盖内置命令
- 修复会话列表删除确认弹框因鼠标移开导致隐藏的问题
- 会话 ID 同步到 URL 参数,刷新页面保持当前会话选中
- 新增命令机制 8 个单元测试
- CATTool 安装改为打开安装页面确认,支持脚本来源追踪和更新提示
- CATToolRepo 索引/数据分离(tools.json 索引 + data/<id>.json 完整记录)
- CATTool UUID 由 SW 统一生成,通过全局映射支持 GM API 权限验证
- AgentModelConfig 从 SystemConfig 迁移到独立的 AgentModelRepo
- AgentModelRepo.getDefaultModelId 使用 chrome.storage.local 直接存取
- cancelCATToolInstall 统一为 try/finally 模式
- openCATToolInstallPage 增加 tab.id 空值检查
实现 Agent 框架的 DOM 操作 API,支持两种模式:
- 默认模式:通过 chrome.scripting.executeScript 操作
- trusted 模式:通过 chrome.debugger CDP 实现真实用户输入

新增 8 个 Agent 工具:dom_list_tabs, dom_navigate, dom_read_page,
dom_screenshot, dom_click, dom_fill, dom_scroll, dom_wait_for

readPage 支持 summary/detail 分层读取,控制上下文大小;
操作后自动返回 ActionResult(跳转/新 tab/dialog 检测);
debugger 权限放在 optional_permissions 动态申请。
- 新增 Skill 类型定义(SkillSummary, SkillRecord, SkillApiRequest 等)
- SKILL.md 解析器(YAML frontmatter + markdown body)
- SkillRepo OPFS 存储(registry + scripts + references)
- AgentService 集成:loadSkills, installSkill, resolveSkills
- 2 个 meta-tool:execute_skill_tool, read_reference
- GM API: CAT.agent.skills(list/get/install/remove)
- UI: ChatInput 增加 Skills 选择器
- agent-fixtures.ts: Mock LLM 基础设施(context.route 拦截 + OpenAI SSE 响应)
- agent-conversation.spec.ts: 基础对话、Tool Calling、多轮上下文保持
- agent-cattool.spec.ts: CATTool 安装/调用/删除、CATTool + 对话联动
resolveSkills 不再将完整 SKILL.md 正文和 CATTool schema 一次性注入 system message,
改为三层渐进加载:1) 摘要列表 2) load_skill 按需获取 prompt 3) execute_skill_tool/read_reference 按需执行。
execute_skill_tool 新增 skill_name 参数确定工具作用域,CATTool 脚本改为执行时按需加载。
新增 14 个 Skill 系统单元测试。
ephemeral 模式下会话不持久化到 OPFS、不加载内置工具和 Skills,
工具完全由脚本提供,消息历史由脚本端内存管理。
SW 端无状态处理,仅解析 model 配置调用 LLM。
…rompts

实现完整的 MCP (Model Context Protocol) 客户端:
- MCPClient: JSON-RPC 2.0 over HTTP POST,Session ID 管理,认证头
- MCPService: 连接池管理,懒连接,MCP 工具自动注册到 ToolRegistry
- MCPServerRepo: chrome.storage 持久化服务器配置
- GM API: CAT.agent.mcp(SW + Content 双侧),权限验证
- UI: AgentMcp 页面 — 服务器 CRUD、启用/禁用、测试连接
- 单元测试覆盖 MCPClient、MCPToolExecutor、MCPService
agent_model:__default__ 存储的是默认模型 ID(字符串),被 find() 当作
AgentModelConfig 返回,导致 UI 访问 model.apiKey.length 时报 TypeError。
- 解析 SSE delta 中的 reasoning_content 字段,发出 thinking_delta 事件(兼容 deepseek/o-series)
- 将 usage 检查移到 choices 处理之后,修复最后一个 chunk 同时包含 tool_call 增量和 usage 时丢失数据的问题
- Service Worker callLLM() 收集 thinking 内容并持久化到 assistant 消息
- 修复 vitest.config.ts 中重复 exclude 导致 e2e 测试被误执行的问题
- 注册 installSkill/removeSkill 消息处理到 AgentService.init()
- AgentClient 新增 installSkill/removeSkill 方法
- 新建 AgentSkills.tsx:Skill 列表卡片、详情编辑弹窗、URL/粘贴安装弹窗
- 替换 /agent/skills 路由为实际管理页面
- 8 个语言文件新增 19 个 agent_skills_* 翻译 key
- 新增 9 个单元测试覆盖安装/卸载/消息注册核心流程
新增 parseSkillZip 解析 ZIP 包(SKILL.md + tools/*.js + references/*),
UI 增加 ZIP 上传 Tab,支持嵌套目录结构,含完整单元测试和端到端集成测试。
chrome.permissions.request() 在 MV3 Service Worker 中无法调用(需要用户手势),
改为打开 confirm.html 确认页面,用户点击授权按钮后通过 runtime.sendMessage 通知 SW。
将三层 meta-tool 设计(load_skill → execute_skill_tool → CATTool)简化为两层:
- load_skill 调用时动态注册该 skill 的 CATTool 为独立工具({skill}__{tool} 格式)
- callLLMWithToolLoop 每轮重新获取工具定义以发现新注册的工具
- 对话结束后清理动态注册的工具和 meta-tools
- 新增 prefixToolDefinition 辅助函数
- 新增 ServerDetailDrawer:展示 Tools/Resources/Prompts 三大能力
- 卡片操作栏增加"详情"按钮,通过 MCPClient 直接查询服务器能力
- 测试连接按钮增加 loading 状态
- 编辑 Modal 底部增加测试连接按钮,填完表单可直接测试
- 补充 8 个 locale 文件的 i18n key
CodFrm added 3 commits April 5, 2026 19:37
为后续 AgentService 拆分铺路:
- LLMProvider 抽象接口 + registry 机制(解耦 OpenAI/Anthropic)
- ToolRegistry 引入来源追踪(builtin/mcp/skill/script)+ scoped 注册
- HTML 搜索引擎插件化(html_extractor 396 → 332 行)
- 拆分 3296 行 agent.test.ts 为 8 个主题测试文件
将 2534 行的 AgentService 上帝对象拆分为 11 个专用服务
(AgentService 472 行,-81%),并重组 Agent 代码目录。

提取的服务:
- SkillService: ZIP 安装/刷新/解析
- AgentTaskService: 定时任务 CRUD + 执行
- AgentModelService: 模型配置与选择
- AgentOPFSService: OPFS 文件访问
- SubAgentService: 子代理会话管理
- BackgroundSessionManager: 后台会话状态
- CompactService: 对话压缩摘要
- LLMClient: LLM 调用封装
- ToolLoopOrchestrator: 工具循环编排
- ChatService: 对话处理(ephemeral/compact/persistent)
- resolveAttachments utility 抽取

目录重组:
- src/app/service/agent/core/          跨上下文共享核心
- src/app/service/agent/service_worker/ SW 端有状态服务
- 清理文件 agent 前缀(agent_dom.ts → dom.ts 等)
AgentTaskRun 原本存在 chrome.storage.local(键前缀 agent_task_run:),
存在几个问题:配额压力、listRuns 全表扫描、缓存常驻内存。

改为 OPFS 后:
- 存储路径:agents/task_runs/{taskId}.json(内含 run[] 降序)
- listRuns O(全表) → O(单文件 parse)
- 环形缓冲:每任务保留最近 100 条(append 时自动裁剪)
- Agent 体系存储后端统一到 OPFS

改动:
- agent_task.ts: AgentTaskRunRepo extends Repo → extends OPFSRepo
- task_scheduler.ts: updateRun 签名加 taskId 参数
- 测试:添加 OPFS mock + 2 个新用例(环形缓冲 / 更新不存在 id)

Agent 功能尚未发布,无需考虑旧数据迁移。
Typecheck 0 错误,1736 测试全绿(+2 新用例)
@cyfung1031
Copy link
Copy Markdown
Collaborator

cyfung1031 commented Apr 5, 2026

Screenshot 2026-04-05 at 20 51 16

UI 显示有问题 原來有滾動條

- 移除未使用的导入(AgentTask、sendMessage、ToolSource、部分 test helpers)
- html_extractor.ts: inline import() 类型改为顶部 import type
- prettier 格式化修复(with_timeout.ts、web_search.ts 等多处)
CodFrm added 9 commits April 5, 2026 20:57
package.json 限制 sideEffects 到 CSS 文件,生产构建时 rspack 会
tree-shake 纯副作用导入。原本 providers/index.ts 用
\`import "./openai"\` 的副作用导入触发 provider 注册,在生产
bundle 中被剥离,导致运行时报 "Unsupported LLM provider: openai"。

修复:把内置 provider 注册移到 registry.ts 模块内,与
providerRegistry 单例同模块。消费者 import providerRegistry 时
必然会触发 registry.ts 的顶层语句,注册就一定发生。

验证:
- dist bundle 现在包含 d7.register({name:"openai",...}) 和
  d7.register({name:"anthropic",...})
- 6 个 agent e2e 测试全部恢复通过
- 1736/1736 单元测试通过
AgentChatRepo 是 OPFS 的无状态包装,原本通过 AgentService._repo
配合 setter 同步给 4 个子服务 (CompactService/LLMClient/
ToolLoopOrchestrator/ChatService),新增子服务容易遗漏同步。

改为模块级单例 agentChatRepo,子服务直接 import 使用,测试用
vi.hoisted + vi.mock 替换整个模块,彻底消除 setter 同步仪式。
- SessionToolRegistry 组合模式:per-session 工具注册隔离,消除并发会话工具覆盖
- excludeTools 后端强校验 + 未知 sub-agent type 抛错
- ChatStreamEvent 分类重组:LLMStreamEvent → ForwardableEvent 层次化,
  消除递归 sub_agent_event 改为 subAgent? 扁平标识
- Sub-agent context OPFS 持久化,resume 时内存优先、OPFS 回退
- 清理 AgentService 测试兼容性代理 getter/setter(~45 行)
- 移除 ChatService 未使用的 compactService 字段
- 提取 OPFS mock 到共享 test-helpers
- 子代理使用独立 childRegistry 替代共享父 sessionRegistry,避免工具泄漏
- system prompt 增加防注入/防数据外泄/数据指令分离安全规则
- get_tab_content 增加 URL 策略校验(assertDomUrlAllowed)
- researcher 子代理移除 execute_script 工具
- Anthropic provider 对畸形 toolCall arguments 降级为空对象
- 子代理超时后追加提示信息,区分用户取消与超时
- tool loop 移除 withRetry 包装,重试逻辑下沉到 llm_client
- 修正 tool call budget 描述为 rounds 而非单次调用
- 新增 navigate_tab 工具:导航已有标签页到新 URL,支持等待加载完成
- researcher 子代理增加页面只读能力(get_tab_content/open_tab/list_tabs/close_tab/navigate_tab),但不允许 DOM 交互
- 移除 get_task 和 delete_task 工具,list_tasks 改为返回完整任务信息
- 页面交互指南仅在同时拥有 get_tab_content 和 execute_script 时显示
@CodFrm CodFrm mentioned this pull request Apr 6, 2026
CodFrm added 5 commits April 6, 2026 22:23
- Skill 支持 version 字段(SkillSummary/SkillMetadata/SkillRecord)
- 新增 URL 安装流程:以 SKILL.cat.md 为入口,按相对路径 fetch scripts/references
- 新增更新检查机制:checkForUpdates/updateSkill,基于 semver 比较
- SKILL.cat.md 优先于 SKILL.md(parseSkillZip 兼容)
- DNR 拦截 .cat.md URL,安装页自动识别并走 URL 安装流程
- SkillInstallView 展示 version 和 installUrl
- AgentSkills 页面支持 URL 安装输入和批量检查更新
- 8 个语言文件新增 i18n keys
- 新增单元测试覆盖 version/URL 安装/更新检查
- 提取 TokenUsage 公共类型,消除 6 处内联重复
- AgentTask 改为判别联合类型(InternalAgentTask | EventAgentTask)
- zhipu 注册为正式 Provider,消除 llm_client 硬编码分支
- handleConversationChat 拆分为 5 个职责单一的子方法
- Provider 层提取 content_utils.ts 公共逻辑(SSE 读取、内容块转换、附件 ID 生成)
- agentChatRepo 统一构造函数注入(LLMClient/CompactService/ToolLoop/ChatService)
- skill_service 提取 fetchSkillResources 共享方法
- web_search 搜索方法模板抽取(fetchAndExtract)
- 统一工具参数校验辅助函数(param_utils.ts)
- 提取 UA/超时常量,tab_tools 复用 stripHtmlTags
- 删除目录重组遗留的旧文件
- 修复 e2e agent-fixtures 缺失 expect 导出
- 简化 agent-chat.spec.ts:移除不可靠的 LLM mock 测试,保留基础 UI 验证
- 修复 agent-fixtures.ts 缺失的 expect 导出
- 提交 e2e/utils.ts 的 Agent 辅助函数(openAgentChatPage 等)
- 删除 debug-provider 调试文件
- pre-commit:每次提交前运行 typecheck + eslint --fix
- pre-push:仅推送到 main/release 时运行测试
- 修正原 pre-commit 实际为 pre-push 逻辑的问题
- 移除 lint-staged(直接全量 lint-fix)
@CodFrm CodFrm requested a review from Copilot April 7, 2026 06:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

该 PR 为 ScriptCat 引入完整的 AI Agent 平台能力,使用户/脚本可通过自然语言对话驱动浏览器自动化(DOM 操作、工具调用、Skill 系统、MCP 扩展、定时任务、OPFS 工作区等),并补齐了核心模块的单测与 E2E 覆盖。

Changes:

  • 新增会话级工具隔离、子代理(Sub-Agent)类型系统、工具集(web_fetch/web_search/execute_script/ask_user/task_tools)、SSE 解析与 Provider 注册体系。
  • 引入 OPFS 工作区与多类 Repo 持久化(对话/附件/任务运行/Skill/子代理上下文),并扩展 MCP 客户端与工具桥接。
  • 新增/重构大量单元测试与 Playwright E2E fixture/用例;更新 ESLint grant 兼容表、CI 脚本与依赖。

Reviewed changes

Copilot reviewed 95 out of 216 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/app/service/agent/service_worker/compact_service.ts 新增对话自动 compact 与内容摘要能力(SW 侧调用 LLM)
src/app/service/agent/service_worker/background_session_manager.ts 管理后台运行会话的流式状态与 UI attach/broadcast
src/app/service/agent/service_worker/autocompact.test.ts compact(手动/自动)行为测试
src/app/service/agent/core/tools/web_search.ts 新增 web_search 工具(多引擎 + Offscreen 提取)
src/app/service/agent/core/tools/web_fetch.ts 新增 web_fetch 工具(抓取 + Offscreen 提取 + 可选摘要)
src/app/service/agent/core/tools/task_tools.ts 新增任务管理工具集(create/update/list)
src/app/service/agent/core/tools/task_tools.test.ts task_tools 单测
src/app/service/agent/core/tools/sub_agent.ts 新增子代理 tool(agent)封装
src/app/service/agent/core/tools/sub_agent.test.ts sub_agent 单测
src/app/service/agent/core/tools/search_config.ts 新增搜索引擎配置 Repo(storage.local)
src/app/service/agent/core/tools/param_utils.ts 新增工具参数校验/转换 util
src/app/service/agent/core/tools/execute_script.ts 新增 execute_script 工具(page/sandbox + 超时)
src/app/service/agent/core/tools/execute_script.test.ts execute_script 单测
src/app/service/agent/core/tools/ask_user.ts 新增 ask_user 工具(UI 提问 + 等待回复/超时)
src/app/service/agent/core/tools/ask_user.test.ts ask_user 单测
src/app/service/agent/core/tool_call_guard.ts 新增工具调用循环/重复检测与系统告警
src/app/service/agent/core/task_scheduler.ts 新增定时任务调度器(cron + run 记录)
src/app/service/agent/core/system_prompt.test.ts system prompt 组装的单测补齐
src/app/service/agent/core/sub_agent_types.ts 新增子代理类型/权限白黑名单与解析逻辑
src/app/service/agent/core/sub_agent_types.test.ts sub_agent_types 单测
src/app/service/agent/core/sse_parser.ts 新增 SSE 文本流解析器
src/app/service/agent/core/sse_parser.test.ts SSEParser 单测
src/app/service/agent/core/skill_script_executor.ts 新增 SkillScript 执行器(Offscreen->Sandbox + 超时 + grants 映射)
src/app/service/agent/core/session_tool_registry.ts 新增会话级 ToolRegistry(隔离并发会话闭包污染)
src/app/service/agent/core/providers/types.ts 新增 Provider 抽象接口类型
src/app/service/agent/core/providers/registry.ts 新增 ProviderRegistry,并注册 openai/anthropic/zhipu
src/app/service/agent/core/providers/registry.test.ts ProviderRegistry 单测
src/app/service/agent/core/providers/openai.ts OpenAI 兼容请求构建与流解析(含 tool_calls/图片块)
src/app/service/agent/core/providers/index.ts providers 导出入口(registry 单例)
src/app/service/agent/core/providers/content_utils.ts Provider 侧 SSE 读取公共骨架与块转换辅助
src/app/service/agent/core/opfs_helpers.ts 新增 OPFS workspace 辅助(sanitizePath/写文件/dataURL 解码)
src/app/service/agent/core/model_context.ts 新增模型上下文窗口推断/覆盖逻辑
src/app/service/agent/core/model_context.test.ts model_context 单测
src/app/service/agent/core/mcp_tool_executor.ts MCP 工具执行器桥接(图片内容转附件)
src/app/service/agent/core/mcp_tool_executor.test.ts MCPToolExecutor 单测
src/app/service/agent/core/mcp_client.ts MCP JSON-RPC over HTTP 客户端实现
src/app/service/agent/core/content_utils.ts MessageContent/ContentBlock 统一处理与 MIME 扩展名工具
src/app/service/agent/core/content_utils.test.ts content_utils 单测
src/app/service/agent/core/compact_prompt.ts compact 提示词与 提取逻辑
src/app/service/agent/core/compact_prompt.test.ts compact_prompt 单测
src/app/service/agent/core/attachment_resolver.ts 解析消息中的 vision 图片附件为 data URL(供 provider)
src/app/repo/test-helpers.ts 抽取可复用的 OPFS mock(供 repo 测试)
src/app/repo/sub_agent_context.ts 新增子代理上下文 OPFS Repo(LRU 上限)
src/app/repo/sub_agent_context.test.ts SubAgentContextRepo 单测
src/app/repo/skill_repo.ts 新增 Skill OPFS Repo(registry + data/scripts/references/config)
src/app/repo/opfs_repo.ts 新增 OPFSRepo 基类(JSON 读写/列文件/删目录)
src/app/repo/mcp_server_repo.ts 新增 MCP server 配置 Repo(storage.local)
src/app/repo/agent_task.ts 新增定时任务 Repo(storage + run OPFS 保存)
src/app/repo/agent_task.test.ts AgentTaskRepo/RunRepo 单测
src/app/repo/agent_model.ts 新增模型配置 Repo(storage + default/summary key)
src/app/repo/agent_chat.ts 新增对话/消息/附件/会话任务 OPFS Repo(含旧路径兼容)
src/app/repo/agent_chat.test.ts AgentChatRepo 附件路径/清理等单测
src/app/cache_key.ts 新增 Skill 安装缓存 key
packages/eslint/linter-config.ts ESLint globals 增加 CAT
packages/eslint/compat-grant.js 增加 CAT.agent.* grant 兼容映射
package.json 更新测试脚本参数与新增 markdown/highlight 相关依赖,调整 uglify-js 位置
example/agent/README.md Agent 示例迁移到独立 skills 仓库说明
e2e/utils.ts 抽取 E2E 通用工具(权限自动确认、脚本注入、Agent 页面打开、SSE 构建等)
e2e/options.spec.ts 稳定化侧边栏定位,避免误点子菜单
e2e/gm-api.spec.ts 复用 fixtures/utils,删除重复逻辑
e2e/fixtures.ts 增加两阶段 userScripts fixture + proxy 支持
e2e/agent-skill.spec.ts 新增 Skill 安装/加载/执行的集成 E2E
e2e/agent-provider.spec.ts 新增 Provider 配置 UI E2E
e2e/agent-navigation.spec.ts 新增 Agent 路由导航 E2E
e2e/agent-fixtures.ts Agent 专用两阶段 fixture + mock LLM route
e2e/agent-error-handling.spec.ts 新增错误处理/重试相关 E2E
e2e/agent-conversation.spec.ts 新增 conversation API 的 E2E(多轮/工具调用)
e2e/agent-chat.spec.ts 新增 Agent Chat 页面基础 E2E
.husky/pre-push 新增按分支触发的 pre-push 测试钩子
.husky/pre-commit 精简为 lint-fix,并支持 SKIP_PRE_COMMIT
.github/workflows/test.yaml workflow 触发分支调整(移除 feature/*)
.github/workflows/build.yaml workflow 触发分支调整(移除 feature/*)
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +28 to +30
this.currentEvent = "";
this.currentData = [];
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"空行结束事件"时只在 currentData.length>0 才重置 currentEvent/currentData,会导致出现仅包含 event 字段但没有 data 的事件后,currentEvent 残留并污染下一条事件类型(下一条没有 event 字段时会错误继承上一条 event)。建议在遇到空行时无条件重置 currentEvent/currentData;是否要在无 data 时也产出事件可按你们协议决定,但至少要清理状态。

Suggested change
this.currentEvent = "";
this.currentData = [];
}
}
this.currentEvent = "";
this.currentData = [];

Copilot uses AI. Check for mistakes.
if (data instanceof Blob) {
await writable.write(data);
} else if (data instanceof Uint8Array) {
await writable.write(data.buffer as ArrayBuffer);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里对 Uint8Array 直接写入 data.buffer 会忽略 byteOffset/byteLength:当传入的是某个 ArrayBuffer 的“视图切片”时,会把整个底层 buffer 都写入,产生错误内容/额外字节。建议写入一个精确切片的 ArrayBuffer(例如按 byteOffset/byteLength 截取),或直接写入 Uint8Array 本身(若 OPFS writable 支持)。

Suggested change
await writable.write(data.buffer as ArrayBuffer);
await writable.write(data);

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
async execute(args: Record<string, unknown>): Promise<string> {
const query = requireString(args, "query");
const maxResults = Math.min((args.max_results as number) || 5, 10);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max_results 这里直接用类型断言读 args.max_results,若传入无法转为数值的字符串(例如 "abc"),Math.min 会得到 NaN,最终 results.slice(0, NaN) 返回空数组,行为与“默认 5”不一致。建议复用 param_utils.optionalNumber 做稳健解析,并在 NaN/<=0 时回退到默认值。

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +45
/** 格式化搜索结果,区分"无结果"和"提取失败" */
function formatSearchResults(
results: Array<{ title: string; url: string; snippet: string }>,
extractionFailed: boolean,
engine: string
): string {
if (extractionFailed && results.length === 0) {
return JSON.stringify({
results: [],
warning: `Result extraction failed or timed out (engine: ${engine}). Try a different search engine or rephrase the query.`,
});
}
return JSON.stringify(results);
}

/** 搜索结果条目类型 */
type SearchResult = { title: string; url: string; snippet: string };

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

工具返回值在“提取失败”时是对象({results, warning}),正常时却是数组([])。这会让上层解析变得分支化且容易出错。建议统一返回结构(例如始终返回 { results: SearchResult[], warning?: string }),即使正常也包一层 results,warning 可选。

Suggested change
/** 格式化搜索结果,区分"无结果"和"提取失败" */
function formatSearchResults(
results: Array<{ title: string; url: string; snippet: string }>,
extractionFailed: boolean,
engine: string
): string {
if (extractionFailed && results.length === 0) {
return JSON.stringify({
results: [],
warning: `Result extraction failed or timed out (engine: ${engine}). Try a different search engine or rephrase the query.`,
});
}
return JSON.stringify(results);
}
/** 搜索结果条目类型 */
type SearchResult = { title: string; url: string; snippet: string };
/** 搜索结果条目类型 */
type SearchResult = { title: string; url: string; snippet: string };
/** 格式化搜索结果,区分"无结果"和"提取失败",并统一返回结构 */
function formatSearchResults(results: SearchResult[], extractionFailed: boolean, engine: string): string {
return JSON.stringify({
results,
...(extractionFailed && results.length === 0
? {
warning: `Result extraction failed or timed out (engine: ${engine}). Try a different search engine or rephrase the query.`,
}
: {}),
});
}

Copilot uses AI. Check for mistakes.

async execute(args: Record<string, unknown>): Promise<string> {
const url = requireString(args, "url");
const prompt = args.prompt as string | undefined;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WEB_FETCH_DEFINITION 声明 prompt 为 required,但实现里没有对 prompt 做 requireString 校验,导致调用方可以绕过“必须提供 prompt”的约束(并且 summarize 分支不会触发,可能返回大段原文、增加上下文压力)。建议与 schema 对齐:对 prompt 做必填校验(至少非空字符串),或同步调整 schema。

Suggested change
const prompt = args.prompt as string | undefined;
const prompt = requireString(args, "prompt").trim();
if (!prompt) {
throw new Error("Parameter 'prompt' must be a non-empty string");
}

Copilot uses AI. Check for mistakes.
definition: ToolDefinition;
executor: ToolExecutor;
} {
const timeoutMs = deps.timeoutMs ?? EXECUTE_SCRIPT_TIMEOUT_MS;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里超时错误消息硬编码为 "after 30s",但实际超时由 timeoutMs 决定(测试里会覆盖为 50ms)。建议错误消息使用 timeoutMs 动态生成(例如换算成秒/毫秒),避免排障时“看见 30s 但实际更短/更长”的误导。

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +60
const { result, tabId: actualTabId } = await withTimeout(
deps.executeInPage(code, { tabId }),
timeoutMs,
() => new Error("execute_script timed out after 30s")
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里超时错误消息硬编码为 "after 30s",但实际超时由 timeoutMs 决定(测试里会覆盖为 50ms)。建议错误消息使用 timeoutMs 动态生成(例如换算成秒/毫秒),避免排障时“看见 30s 但实际更短/更长”的误导。

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +69
const result = await withTimeout(
deps.executeInSandbox(code),
timeoutMs,
() => new Error("execute_script timed out after 30s")
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里超时错误消息硬编码为 "after 30s",但实际超时由 timeoutMs 决定(测试里会覆盖为 50ms)。建议错误消息使用 timeoutMs 动态生成(例如换算成秒/毫秒),避免排障时“看见 30s 但实际更短/更长”的误导。

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +4
import { supportsVision } from "@App/pages/options/routes/AgentChat/model_utils";

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core 模块依赖 pages/options 下的 UI 路径(supportsVision)会增加跨层耦合,且更容易引入打包体积/循环依赖风险(尤其在 service worker 侧复用时)。建议将 supportsVision 下沉到 agent/core 内的纯函数工具(或共享 util 包)再由 UI 侧复用,保证 core 侧无 UI 依赖。

Suggested change
import { supportsVision } from "@App/pages/options/routes/AgentChat/model_utils";
function supportsVision(model: AgentModelConfig): boolean {
const candidate = model as AgentModelConfig & {
supportsVision?: boolean;
supportVision?: boolean;
vision?: boolean;
capabilities?: {
vision?: boolean;
image?: boolean;
};
modalities?: string[];
};
if (typeof candidate.supportsVision === "boolean") {
return candidate.supportsVision;
}
if (typeof candidate.supportVision === "boolean") {
return candidate.supportVision;
}
if (typeof candidate.vision === "boolean") {
return candidate.vision;
}
if (candidate.capabilities) {
if (typeof candidate.capabilities.vision === "boolean") {
return candidate.capabilities.vision;
}
if (typeof candidate.capabilities.image === "boolean") {
return candidate.capabilities.image;
}
}
if (Array.isArray(candidate.modalities)) {
return candidate.modalities.includes("vision") || candidate.modalities.includes("image");
}
return false;
}

Copilot uses AI. Check for mistakes.
onDisconnect: vi.fn(),
};
const sender = {
isType: (type: any) => type === 1,
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试里用 magic number (type === 1) 模拟 CONNECT sender,会让用例对 GetSenderType.CONNECT 的具体枚举值产生隐式依赖(未来枚举变更会导致测试“无意义失败”)。建议直接引入并使用 GetSenderType.CONNECT 来比较,或在 helper 里参数化该值。

Suggested change
isType: (type: any) => type === 1,
isType: vi.fn(() => true),

Copilot uses AI. Check for mistakes.
CodFrm added 4 commits April 7, 2026 14:48
- SSE 解析器空行时无条件重置 currentEvent,防止残留污染
- OPFS 写入 Uint8Array 时精确截取字节段,修复切片视图 bug
- execute_script 超时消息改为动态生成,匹配实际超时时间
- cleanupIfDone 回调中重新检查会话状态,防止误删已复用会话
- 模型能力检测函数移至 core/model_capabilities.ts,消除 core→UI 反向依赖
- web_search max_results 改用 optionalNumber 防止 NaN
- task_tools subject/description 使用类型安全的参数校验
agent 工具新增 tab_id 可选参数,父代理可将已打开的标签页传递给子代理,
子代理会直接使用该标签页而非重复打开新页面。
pre-commit:lint + main/release/* 分支跑 test:ci
删除 pre-push hook
@CodFrm CodFrm merged commit 94b533d into release/v1.4-agent Apr 7, 2026
4 checks passed
@CodFrm CodFrm deleted the feature/agent branch April 7, 2026 14:25
@cyfung1031
Copy link
Copy Markdown
Collaborator

@CodFrm 我尝试用 skills 给AI看这个PR

它好像找到了问题。你可以研究一下


📋 PR 概要 / PR Summary

PR: #1324 · 💥 脚本猫 AI Agent
作者: CodFrm
变更规模: +30,880 / -2,158 · 212 个文件 · 176 commits
类型: 全新功能平台(巨型 Feature)
状态: ✅ 已合并(2026-04-07)到 release/v1.4-agent


🔍 变更分析 / Change Analysis

整体架构

这是一个从零构建的完整 AI Agent 平台。核心分层清晰:

LLMProvider 抽象层(OpenAI / Anthropic / ZhiPu)
    ↓
ToolRegistry + SessionToolRegistry(会话级隔离)
    ↓
ToolLoopOrchestrator(工具循环调度)
    ↓
ChatService / CompactService(对话管理)
    ↓
AgentService(编排入口,已从 2534 行拆分至 472 行)

重要决策是将持久化统一到 OPFS(对话、附件、任务记录、Skill、子代理上下文),只有轻量配置留在 chrome.storage.local


核心模块逐一评估

sse_parser.ts / SSE 解析器
初版存在空行重置残留污染 bug(currentEvent 未无条件清理),已由 Copilot Review 发现,最终提交中修复。修复方式正确。

session_tool_registry.ts / 会话级工具注册
解决了并发会话间工具命名污染的问题,组合模式设计合理。{skill}__{tool} 命名空间前缀是轻量但有效的隔离方案。潜在风险:如果 skill 名称本身含 __,命名空间解析可能歧义,建议未来用更鲁棒的分隔符。

sub_agent_types.ts / 子代理类型系统
引入判别联合类型(InternalAgentTask | EventAgentTask),类型安全性好。权限白黑名单设计合理:researcher 子代理被限制为只读,不能调用 execute_script,安全边界清晰。

opfs_helpers.ts / OPFS 工具
Uint8Array.buffer 的切片 bug(byteOffset/byteLength 未考虑)已修复。但 sanitizePath 的黑名单过滤策略(../)在路径拼接上依赖调用方规范,仍是一个隐式契约,建议补充更严格的测试用例。

web_search.ts / 搜索工具
formatSearchResults 在成功时返回裸数组、在失败时返回对象的不一致结构已经 Copilot 指出。max_resultsNaN 问题已修复,改为 optionalNumber。不一致返回结构在最新 commit 中是否已修复,日志中未明确确认,需追踪。

web_fetch.ts / 抓取工具
Schema 声明 prompt 为 required 但代码中未强制校验,存在合约不一致。Copilot 的 suggestion 是加 requireString,但合并时 commit 116c5de 的说明未提及此条目,需确认是否已处理。

compact_service.ts / 自动压缩
设计上合理:上下文超阈值时触发 LLM 摘要压缩。风险是压缩本身会消耗 token,在高频对话场景下可能引发"压缩风暴"。建议增加压缩频率限制或冷却机制。

mcp_client.ts / MCP 客户端
JSON-RPC 2.0 over HTTP,Session ID 管理,懒连接池。结构上缺少对 JSON-RPC error 响应体的统一错误包装,目前若 MCP 服务器返回标准错误对象,调用方需自行解析,不如包装为自定义 Error 类型。

skill_service.ts / Skill 系统
三层渐进加载(摘要 → 按需加载完整 prompt → 按需执行工具)是减少 system prompt 膨胀的良好设计。URL 分发机制(SKILL.cat.md 作为入口)和版本管理(semver 比较)做到了工程完整性。ZIP 安装和拖拽 UI 体验加分。

AgentService 拆分
从 2534 行上帝对象 → 11 个专用服务,是这次 PR 最重要的架构改进。agentChatRepo 改为模块单例后消除了 setter fan-out,是正确的方向。目录分层(core/ vs service_worker/)职责清晰。

DOM 操作 / CDP 双引擎
两种模式的权限分级(默认 chrome.scripting / trusted 走 CDP)设计合理。URL 黑名单守卫(chrome://about://devtools:// 等)是必要的安全防线,已在 3f4d4fe 中加入。

测试覆盖
单元测试覆盖了 SSE 解析、工具注册、MCP 客户端、CATTool 执行、Skill 系统、压缩服务等核心模块,是这次 PR 的显著优点。E2E 用了 Playwright + mock LLM route 的两阶段 fixture,覆盖了对话流、工具调用、Skill 安装、错误处理等场景。测试质量整体较高。


⚠️ 问题与风险 / Issues & Risks

🔴 必须追踪确认(已合并但需验证)

web_fetch.ts prompt 参数校验缺失:Schema 标记 prompt 为 required,但实现中是 as string | undefined。如果最终 commit 116c5de 未修复此条,Agent 可能在无 prompt 时静默返回大段原文,增加上下文压力。需在 release 前确认修复。

web_search.ts 返回结构不一致formatSearchResults 成功返回裸数组、失败返回对象的问题,Copilot 提供了 suggestion 但最终 commit 说明未提及此项,需确认是否实际合入修复。

🟡 建议改善

Skill 名称中的 __ 分隔符{skill}__{tool} 的命名前缀在 skill 名包含 __ 时会导致解析歧义。建议改用 UUID 前缀或其他不可能在 skill 名出现的分隔符。

压缩风暴防御compact_service.ts 缺少压缩频率冷却机制,极端情况下每轮对话都触发 LLM 摘要调用。

MCP 错误结构化:JSON-RPC error 响应应包装为统一错误类型,避免调用方各自解析。

sanitizePath 测试覆盖:路径拼接安全仍依赖调用方规范,建议增加针对路径遍历的专项测试。

pre-commit hook 跑完整测试:最后一个 commit 将测试从 pre-push 移至 pre-commit,在大型代码库中 commit 时等待 1700+ 个测试会严重影响开发体验。建议仅在 CI 或 pre-push 时跑完整测试,pre-commit 只跑 lint。

attachment_resolver.tssupportsVision 依赖:原本从 pages/options/routes/AgentChat/model_utils 导入,形成 core→UI 反向依赖。116c5de 的修复摘要提到"模型能力检测函数移至 core/model_capabilities.ts",此项应已解决,但需确认 UI 层已改为从 core 层反向导入。

🟢 可选优化

withTimeout 超时消息动态化:已在 116c5de 修复,确认无遗漏。

agentChatRepo 测试的 vi.hoisted 模式:当前用法正确,但 vi.hoisted 与模块单例在某些 vitest 版本下有时序问题,若未来升级 vitest 需注意。


✅ 优点 / Strengths

架构演进节奏合理:功能从 MVP 快速迭代,每个 commit 粒度清晰(feat/fix/refactor 分开),重构(AgentService 拆分)在功能稳定后才进行,策略正确。

安全意识到位:子代理工具隔离(独立 childRegistry)、system prompt 防注入规则、DOM 操作 URL 黑名单、GM API 权限验证——整体安全防线完整,不是"先做功能后补安全"。

测试质量:1736 个测试(含单元 + E2E),覆盖了核心路径和边界情况,mock LLM SSE route 的设计很实用,E2E 场景选择有代表性。

类型安全:TypeScript 覆盖完整,scriptcat.d.ts +1193 行同步更新,CAT.agent.* GM API 有完整类型定义。

Copilot Review 的快速响应:Copilot 发现的 7 个问题在 24 小时内全部修复(116c5de),体现了良好的工程响应速度。

AgentService 的拆分质量:将 2534 行上帝对象压缩到 472 行(-81%),11 个子服务职责单一,是教科书级别的重构。


📊 总体评估 / Overall Assessment

建议: APPROVE(已合并,事后审查结论一致)

这是一次工程质量高于平均水平的大型 Feature PR。架构设计成熟(Provider 抽象、会话级工具隔离、三层 Skill 加载、OPFS 统一存储),安全边界清晰,测试覆盖有意识地跟进。Copilot Review 发现的问题均已及时修复。

待追踪的两个不确定项(web_fetch prompt 校验、web_search 返回结构)在下一个 release 前需要确认,其余为建议性优化,不阻塞发布。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants